Anleitung zu hashCode () in Java

Anleitung zu hashCode () in Java

1. Überblick

Hashing ist ein grundlegendes Konzept der Informatik.

In Java stehen effiziente Hashing-Algorithmen hinter einigen der beliebtesten verfügbaren Sammlungen - wie z. B.HashMap (für einen detaillierten Blick aufHashMap können Siethis article überprüfen.) und dieHashSet.

In diesem Artikel konzentrieren wir uns darauf, wiehashCode()funktioniert, wie es in Sammlungen abgespielt wird und wie es korrekt implementiert wird.

2. Verwendung vonhashCode() in Datenstrukturen

Die einfachsten Vorgänge für Sammlungen können in bestimmten Situationen ineffizient sein.

Dies löst beispielsweise eine lineare Suche aus, die für Listen mit großen Größen äußerst unwirksam ist:

List words = Arrays.asList("Welcome", "to", "example");
if (words.contains("example")) {
    System.out.println("example is in the list");
}

Java bietet eine Reihe von Datenstrukturen, um dieses Problem speziell zu behandeln. Beispielsweise sind mehrereMap-Schnittstellenimplementierungenhash tables.

Wenn Sie eine Hash-Tabelle verwenden, geben Siethese collections calculate the hash value for a given key using the hashCode() method ein und verwenden Sie diesen Wert intern, um die Daten zu speichern, sodass Zugriffsvorgänge wesentlich effizienter sind.

3. Verstehen, wiehashCode() funktioniert

Einfach ausgedrückt,hashCode() gibt einen ganzzahligen Wert zurück, der von einem Hashing-Algorithmus generiert wird.

Objekte, die gleich sind (entsprechend ihrenequals()), müssen denselben Hashcode zurückgeben. It’s not required for different objects to return different hash codes.

Der Generalvertrag vonhashCode() lautet:

  • Immer wenn es während der Ausführung einer Java-Anwendung mehr als einmal für dasselbe Objekt aufgerufen wird, musshashCode() konsistent denselben Wert zurückgeben, sofern keine Informationen geändert werden, die für gleiche Vergleiche für das Objekt verwendet werden. Dieser Wert muss von einer Ausführung einer Anwendung zu einer anderen Ausführung derselben Anwendung nicht konsistent bleiben

  • Wenn zwei Objekte gemäß derequals(Object)-Methode gleich sind, muss der Aufruf derhashCode()-Methode für jedes der beiden Objekte denselben Wert erzeugen

  • Es ist nicht erforderlich, dass, wenn zwei Objekte gemäß derequals(java.lang.Object)-Methode ungleich sind, der Aufruf derhashCode-Methode für jedes der beiden Objekte unterschiedliche ganzzahlige Ergebnisse liefern muss. Entwickler sollten sich jedoch darüber im Klaren sein, dass die Leistung von Hash-Tabellen verbessert wird, wenn eindeutige Ganzzahlergebnisse für ungleiche Objekte erstellt werden

„Soweit dies vernünftigerweise praktikabel ist, gibt die durch die KlasseObject definiertehashCode()-Methode unterschiedliche Ganzzahlen für unterschiedliche Objekte zurück. (Dies wird normalerweise durch Konvertieren der internen Adresse des Objekts in eine Ganzzahl implementiert. Diese Implementierungstechnik wird jedoch von der JavaTM-Programmiersprache nicht benötigt.)

4. Eine naivehashCode() Implementierung

Es ist eigentlich ganz einfach, eine naivehashCode()-Implementierung zu haben, die den oben genannten Vertrag vollständig einhält.

Um dies zu demonstrieren, definieren wir eine BeispielklasseUser, die die Standardimplementierung der Methode überschreibt:

public class User {

    private long id;
    private String name;
    private String email;

    // standard getters/setters/constructors

    @Override
    public int hashCode() {
        return 1;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null) return false;
        if (this.getClass() != o.getClass()) return false;
        User user = (User) o;
        return id == user.id
          && (name.equals(user.name)
          && email.equals(user.email));
    }

    // getters and setters here
}

Die KlasseUser bietet benutzerdefinierte Implementierungen fürequals() undhashCode(), die die jeweiligen Verträge vollständig einhalten. Darüber hinaus ist es nicht unzulässig, wennhashCode()einen festen Wert zurückgibt.

Diese Implementierung verschlechtert jedoch die Funktionalität von Hash-Tabellen auf grundsätzlich Null, da jedes Objekt in demselben einzelnen Bucket gespeichert würde.

In diesem Zusammenhang wird eine Hashtabellensuche linear durchgeführt und bringt uns keinen wirklichen Vorteil - mehr dazu in Abschnitt 7.

5. Verbesserung der Implementierung vonhashCode()

Verbessern wir die aktuelle Implementierung vonhashCode()ein wenig, indem wir alle Felder der KlasseUsereinbeziehen, damit unterschiedliche Ergebnisse für ungleiche Objekte erzielt werden können:

@Override
public int hashCode() {
    return (int) id * name.hashCode() * email.hashCode();
}

Dieser grundlegende Hashing-Algorithmus ist definitiv viel besser als der vorherige, da er den Hash-Code des Objekts berechnet, indem nur die Hash-Codes der Feldername undemail undid multipliziert werden.

Im Allgemeinen können wir sagen, dass dies eine vernünftigehashCode()-Implementierung ist, solange wir dieequals()-Implementierung damit konsistent halten.

6. StandardhashCode() Implementierungen

Je besser der Hash-Algorithmus ist, den wir zur Berechnung von Hash-Codes verwenden, desto besser ist die Leistung von Hash-Tabellen.

Schauen wir uns eine "Standard" -Implementierung an, die zwei Primzahlen verwendet, um berechneten Hash-Codes noch mehr Eindeutigkeit zu verleihen:

@Override
public int hashCode() {
    int hash = 7;
    hash = 31 * hash + (int) id;
    hash = 31 * hash + (name == null ? 0 : name.hashCode());
    hash = 31 * hash + (email == null ? 0 : email.hashCode());
    return hash;
}

Während es wichtig ist, die Rollen zu verstehen, die die MethodenhashCode() undequals()pielen, müssen wir sie nicht jedes Mal von Grund auf neu implementieren, da die meisten IDEs benutzerdefiniertehashCode() undequals()generieren können ) s Implementierungen und seit Java 7 haben wir eineObjects.hash() Dienstprogrammmethode für komfortables Hashing:

Objects.hash(name, email)

IntelliJ IDEA generiert die folgende Implementierung:

@Override
public int hashCode() {
    int result = (int) (id ^ (id >>> 32));
    result = 31 * result + name.hashCode();
    result = 31 * result + email.hashCode();
    return result;
}

UndEclipse erzeugt dieses:

@Override
public int hashCode() {
    final int prime = 31;
    int result = 1;
    result = prime * result + ((email == null) ? 0 : email.hashCode());
    result = prime * result + (int) (id ^ (id >>> 32));
    result = prime * result + ((name == null) ? 0 : name.hashCode());
    return result;
}

Zusätzlich zu den oben genannten IDE-basiertenhashCode()-Implementierungen ist es auch möglich, automatisch eine effiziente Implementierung zu generieren, beispielsweise mitLombok. In diesem Fall muss die Abhängigkeit vonlombok-mavenzupom.xml addiert werden:


    org.projectlombok
    lombok-maven
    1.16.18.0
    pom

Es reicht jetzt aus, die KlasseUsermit@EqualsAndHashCodezu versehen:

@EqualsAndHashCode
public class User {
    // fields and methods here
}

WennApache Commons Lang’s HashCodeBuilder class einehashCode()-Implementierung für uns generieren soll, muss die Maven-Abhängigkeit voncommons-lang in die pom-Datei aufgenommen werden:


    commons-lang
    commons-lang
    2.6

UndhashCode() können folgendermaßen implementiert werden:

public class User {
    public int hashCode() {
        return new HashCodeBuilder(17, 37).
        append(id).
        append(name).
        append(email).
        toHashCode();
    }
}

Im Allgemeinen gibt es kein universelles Rezept, an das Sie sich halten können, wenn SiehashCode() implementieren. Wir empfehlen dringend,Joshua Bloch’s Effective Java zu lesen, die eine Liste vonthorough guidelines für die Implementierung effizienter Hashing-Algorithmen enthält.

Was hier bemerkt werden kann, ist, dass alle diese Implementierungen in irgendeiner Form die Zahl 31 verwenden - dies liegt daran, dass 31 eine nette Eigenschaft hat - seine Multiplikation kann durch eine bitweise Verschiebung ersetzt werden, die schneller ist als die Standardmultiplikation:

31 * i == (i << 5) - i

7. Umgang mit Hash-Kollisionen

Das intrinsische Verhalten von Hash-Tabellen wirft einen relevanten Aspekt dieser Datenstrukturen auf: Selbst mit einem effizienten Hashing-Algorithmus können zwei oder mehr Objekte denselben Hash-Code haben, selbst wenn sie ungleich sind. Ihre Hash-Codes würden also auf denselben Bucket verweisen, obwohl sie unterschiedliche Hash-Tabellenschlüssel hätten.

Diese Situation ist allgemein als Hash-Kollision undvarious methodologies exist for handling it bekannt, wobei jede ihre Vor- und Nachteile hat. JavaHashMapverwendetthe separate chaining method zur Behandlung von Kollisionen:

„Wenn zwei oder mehr Objekte auf denselben Bucket verweisen, werden sie einfach in einer verknüpften Liste gespeichert. In einem solchen Fall ist die Hash-Tabelle ein Array von verknüpften Listen, und jedes Objekt mit demselben Hash wird an die verknüpfte Liste am Bucket-Index im Array angehängt.

In the worst case, several buckets would have a linked list bound to it, and the retrieval of an object in the list would be performed linearly. ”

Hash-Kollisionsmethoden zeigen auf den Punkt, warum es so wichtig ist,hashCode() effizient. zu implementieren

Java 8 brachte interessanteenhancement to HashMap implementation - wenn eine Bucket-Größe den bestimmten Schwellenwert überschreitet, wird die verknüpfte Liste durch eine Baumkarte ersetzt. Dies ermöglicht das Erreichen vonO(logn _) _ Lookup anstelle von pessimistischenO(n).

8. Erstellen einer Trivial-Anwendung

Um die Funktionalität einer Standardimplementierung vonhashCode()zu testen, erstellen wir eine einfache Java-Anwendung, dieUser Objekte zuHashMap hinzufügt undSLF4J zum Protokollieren einer Nachricht an der Konsole verwendet jedes Mal, wenn die Methode aufgerufen wird.

Hier ist der Einstiegspunkt der Beispielanwendung:

public class Application {

    public static void main(String[] args) {
        Map users = new HashMap<>();
        User user1 = new User(1L, "John", "[email protected]");
        User user2 = new User(2L, "Jennifer", "[email protected]");
        User user3 = new User(3L, "Mary", "[email protected]");

        users.put(user1, user1);
        users.put(user2, user2);
        users.put(user3, user3);
        if (users.containsKey(user1)) {
            System.out.print("User found in the collection");
        }
    }
}

Und dies ist die Implementierung vonhashCode():

public class User {

    // ...

    public int hashCode() {
        int hash = 7;
        hash = 31 * hash + (int) id;
        hash = 31 * hash + (name == null ? 0 : name.hashCode());
        hash = 31 * hash + (email == null ? 0 : email.hashCode());
        logger.info("hashCode() called - Computed hash: " + hash);
        return hash;
    }
}

Das einzige Detail, das hier hervorgehoben werden sollte, ist, dass jedes Mal, wenn ein Objekt in der Hash-Map gespeichert und mit der MethodecontainsKey() überprüft wird,hashCode() aufgerufen und der berechnete Hash-Code auf der Konsole ausgedruckt wird:

[main] INFO com.example.entities.User - hashCode() called - Computed hash: 1255477819
[main] INFO com.example.entities.User - hashCode() called - Computed hash: -282948472
[main] INFO com.example.entities.User - hashCode() called - Computed hash: -1540702691
[main] INFO com.example.entities.User - hashCode() called - Computed hash: 1255477819
User found in the collection

9. Fazit

Es ist klar, dass das Produzieren effizienterhashCode()-Implementierungen häufig eine Mischung einiger mathematischer Konzepte erfordert (d. H. Primzahlen und beliebige Zahlen), logische und grundlegende mathematische Operationen.

Unabhängig davon ist es durchaus möglich,hashCode() effektiv zu implementieren, ohne auf diese Techniken zurückzugreifen, solange wir sicherstellen, dass der Hashing-Algorithmus unterschiedliche Hash-Codes für ungleiche Objekte erzeugt und mit der Implementierung vonequals() übereinstimmt.

Wie immer sind alle in diesem Artikel gezeigten Codebeispieleover on GitHub verfügbar.